package hexapawn.main;

import hexapawn.game.Game;
import hexapawn.io.UIOAction;
import hexapawn.io.UIOManager;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.awt.event.WindowListener;
import java.awt.event.WindowStateListener;
import java.text.DecimalFormat;
import javax.swing.JFrame;

/**
 * This is the main class handles the Graphics part of the game. It extends JFrame and
 * directly draws on it. This of course mean Menus cannot be drawn since they will be
 * covered by other graphics object.
 * 
 * @author ChaoSXDemon
 *
 */
@SuppressWarnings("serial")
public class Hexapawn extends JFrame implements Runnable, ComponentListener, WindowListener, WindowStateListener, WindowFocusListener{

	private static final int NO_DELAYS_PER_YIELD = 16;	//Number of frames with 0ms delay before yield
	private static final int NUM_FRAMES = 10;	//Numer of frames to use to calculate avg fps and ups
	private static final long MAX_STATS_INTERVAL = 1000000000L;	//Every second
	private static int MAX_FRAME_SKIPS = 5;
	
	//Control Variables
	private static int pWidth;
	private static int pHeight;
	private Thread animator;
	private volatile boolean running = false;
	private volatile boolean gameOver = false;
	private volatile boolean isPaused = false;
	private Font font;
	private FontMetrics metrics;
	private DecimalFormat df = new DecimalFormat("0.##");  // 2 dp
	
	//Rendering Variables
	private Graphics dbg;
	private Image dbImage;
	private static final Color CLEAR_COLOR = Color.DARK_GRAY;
	
	//Timing Variables
	private long period;
	private long gameStartTime;
	private long statsInterval = 0L, prevStatsTime;
	private long totalElapsedTime = 0L;
	private long frameCount = 0L, statsCount = 0L;
	private long framesSkipped = 0L, totalFramesSkipped = 0L;
	private double upsData[];
	private double fpsData[];
	private int timeSpentInGame = 0;
	private double avgFPS = 0d, avgUPS = 0d;
	
	//User io variables
	private UIOManager manager;
	private UIOAction exitAction;
	private UIOAction restartAction;
	
	//Game variables
	private Game hexapawn;
	private int row, col;
	
	public Hexapawn(String name, int row, int col){
		super(name);
		pWidth = 800;
		pHeight = 600;
		manager = new UIOManager(this);
		
		initWindow();
		initActions();
		
		period = (long)((1000.0 / 60.0)*1000000L);	//60 fps
		font = new Font("sansSerif", Font.BOLD, 20);
		metrics = getFontMetrics(font);
		
		upsData = new double[NUM_FRAMES];
		fpsData = new double[NUM_FRAMES];
		for(int i=0; i<NUM_FRAMES; i++)	upsData[i] = fpsData[i] = 0d;
		
		hexapawn = new Game(this, row, col);
		this.row = row;
		this.col = col;
		
		startGame();
	}
	
	private void initWindow(){
		//Add window listeners
		addWindowListener(this);
		addWindowFocusListener(this);
		addWindowStateListener(this);
		addComponentListener(this);
		
		setSize(pWidth, pHeight);
		//setResizable(false);
		setFocusable(true);
		//setUndecorated(true);	//No menu bar or borders
		setIgnoreRepaint(true);	//We use active rendering
		setVisible(true);
		requestFocus();
	}
	
	private void initActions(){
		exitAction = new UIOAction("Exit");
		manager.maptoKey(exitAction, KeyEvent.VK_ESCAPE);
		restartAction = new UIOAction("Restart");
		manager.maptoKey(restartAction, KeyEvent.VK_R);
	}

	private final void startGame(){
		if(animator == null || !running){
			animator = new Thread(this);
			animator.start();
		}
	}
	
	private final void gameUpdate(){
		if(!isPaused ){
			if(!gameOver){
				//Check for user input
				if(exitAction.isPressed()) stopGame();
				hexapawn.update();
			}
			if(restartAction.isPressed()){
				hexapawn = new Game(this, row, col);
				gameOver = false;
			}
		}
	}
	
	private final void gameRender(){
		if(dbImage == null)
			dbImage = createImage(pWidth, pHeight);
		if(dbImage == null){
			System.out.println("dbImage is null");
			return;
		}else dbg = dbImage.getGraphics();
		
		dbg.setColor(CLEAR_COLOR);
		dbg.fillRect(0, 0, pWidth, pHeight);
		
		//Draw Game Element
		hexapawn.draw(dbg);
		
		//Draw Stats
		dbg.setColor(Color.WHITE);
		dbg.setFont(font);
		dbg.drawString("Average FPS/UPS: " + df.format(avgFPS) + "/" + df.format(avgUPS), 25, 50);
		dbg.drawString("Mouse @ ("+manager.getMouseX()+","+manager.getMouseY()+")", 25, 75);
			//g.drawString(GAME_PATH, 500, 300);
	}
	
	private final void paintScreen(){
		Graphics g;
		try{
			g = this.getGraphics();
			if((g!=null) &&(dbImage != null))
				g.drawImage(dbImage, 0, 0, null);
			g.dispose();
		}catch(Exception e){
			System.err.println("Graphics error: " + e);
		}
	}
	
	public void pauseGame(){
		isPaused = true;
	}
	
	public void resumeGame(){
		isPaused = false;
	}
	
	public void stopGame(){
		running = false;
	}
	
	public UIOManager getManager(){
		return manager;
	}
	
	@Override
	public void run(){
		//Timing Variables
		long beforeTime, afterTime, timeDiff, sleepTime;
		long overSleepTime = 0L, excess = 0L;
		int noDelays = 0;
		
		//Stats timing
		gameStartTime = System.nanoTime();
		prevStatsTime = gameStartTime;
		beforeTime = gameStartTime;

		running = true;
		while(running){
			gameUpdate();
			gameRender();
			paintScreen();
			
			//Calculate sleep time
			afterTime = System.nanoTime();
			timeDiff = afterTime - beforeTime;
			sleepTime = (period - timeDiff) - overSleepTime;
			
			if(sleepTime > 0){	//There is some time for sleep
				try{
					Thread.sleep(sleepTime/1000000L);	//nano->ms
				}catch(InterruptedException ex){}
				overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
			}else{	//Frame took longer than period
				excess -= sleepTime;	//Stores excess time. Note that sleepTime < 0
				overSleepTime = 0L;
				if(++noDelays >= NO_DELAYS_PER_YIELD){
					Thread.yield();
					noDelays = 0;
				}
			}
			beforeTime = System.nanoTime();
			
			//Use update to catch up if FPS is too slow
			int skips = 0;
			while( (excess > period) && (skips < MAX_FRAME_SKIPS) ){
				excess -= period;
				gameUpdate();
				skips++;
			}
			
			framesSkipped += skips;
			
			storeStats();
		}
		printStats();
		System.exit(0);
	}
	
	@Override
	public void windowGainedFocus(WindowEvent e){
		resumeGame();
	}

	@Override
	public void windowLostFocus(WindowEvent e){
		pauseGame();
	}

	@Override
	public void windowStateChanged(WindowEvent e){
		System.out.println(e);
	}

	@Override
	public void windowActivated(WindowEvent e){
		resumeGame();
	}

	@Override
	public void windowClosed(WindowEvent e){
		stopGame();
		System.out.println("Stopped");
	}

	@Override
	public void windowClosing(WindowEvent e){
		System.out.println("Closing...");
		windowClosed(e);
	}

	@Override
	public void windowDeactivated(WindowEvent e){
		pauseGame();
	}

	@Override
	public void windowDeiconified(WindowEvent e){
		//Window goes back to be a window
		resumeGame();
	}

	@Override
	public void windowIconified(WindowEvent e){
		//Window becomes an icon...
		pauseGame();
	}

	@Override
	public void windowOpened(WindowEvent e){
		System.out.println("Game Started...");
	}

	public void componentHidden(ComponentEvent e) {
        //System.out.println(e.getComponent().getClass().getName() + " --- Hidden");
    }

    public void componentMoved(ComponentEvent e) {
    	//System.out.println(e.getComponent().getClass().getName() + " --- Moved");
    }

    public void componentResized(ComponentEvent e) {
    	//System.out.println(e.getComponent().getClass().getName() + " --- Resized ");
    	if(dbImage != null){
	    	double w = e.getComponent().getSize().getWidth();
	    	double h = e.getComponent().getSize().getHeight();
	    	pWidth = (int)w;
	    	pHeight = (int)h;
			dbImage = createImage(pWidth, pHeight);
			if(dbImage == null)	System.err.println("Failed to Resize");
			dbg = dbImage.getGraphics();
			System.out.println("Resized to: ("+w+","+h+")");
			hexapawn.resize();
    	}
    }

    public void componentShown(ComponentEvent e) {
    	//System.out.println(e.getComponent().getClass().getName() + " --- Shown");
    }
    
	public static void main(String[] args){
		new Hexapawn("Hexapawn v0.7b", 5, 5);
	}
	
	private void printStats(){
	    System.out.println("Frame Count/Loss: " + frameCount + " / " + totalFramesSkipped);
		System.out.println("Average FPS: " + df.format(avgFPS));
		System.out.println("Average UPS: " + df.format(avgUPS));
	    System.out.println("Time Spent: " + timeSpentInGame + " secs");		
	} 
	
	private void storeStats(){
		frameCount++;
		statsInterval += period;
		
		if(statsInterval >= MAX_STATS_INTERVAL){
			long timeNow = System.nanoTime();
			timeSpentInGame = (int)((timeNow-gameStartTime)/1000000000L);
			
			long realElapsedTime = timeNow - prevStatsTime;	//Time since last stats collection
			totalElapsedTime += realElapsedTime;
			
			//double timingError = ((double)(realElapsedTime-statsInterval)/statsInterval)*100.0;
			totalFramesSkipped += framesSkipped;
			
			double actualFPS = 0;
			double actualUPS = 0;
			if(totalElapsedTime > 0){
				actualFPS = (((double)frameCount / totalElapsedTime) * 1000000000L);
				actualUPS = (((double)(frameCount+totalFramesSkipped)/totalElapsedTime)*1000000000L);
			}
			//Store
			fpsData[(int)statsCount%NUM_FRAMES] = actualFPS;
			upsData[(int)statsCount%NUM_FRAMES] = actualUPS;
			statsCount++;
			
			//Calculate average
			double totalFPS = 0d;
			double totalUPS = 0d;
			for(int i=0; i < NUM_FRAMES; i++){
		        totalFPS += fpsData[i];
		        totalUPS += upsData[i];
			}
			if(statsCount < NUM_FRAMES){
				avgFPS = totalFPS / statsCount;
				avgUPS = totalUPS / statsCount;
			}else{
				avgFPS = totalFPS / NUM_FRAMES;
				avgUPS = totalUPS / NUM_FRAMES;
			}
			
			//Clearn up
			framesSkipped = 0;
			prevStatsTime = timeNow;
			statsInterval = 0L;
		}
	}
	
}
